<!DOCTYPE html>
<html>
<!--
Copyright 2011 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
<title>Element Bindings Tests</title>
<script src="third_party/closure/closure/goog/base.js"></script>
<script src="platform/compat.js"></script>
<script src="platform/weak_map.js"></script>
<script src="path.js"></script>
<script src="model.js"></script>
<script src="transform.js"></script>
<script src="dependency_parser.js"></script>
<script src="expression_parser.js"></script>
<script src="bind_attribute_parser.js"></script>
<script src="place_holder_parser.js"></script>
<script src="element_model.js"></script>
<script src="html5_attributes.js"></script>
<script src="element_bindings.js"></script>
<script src="test_common.js"></script>

<script>
goog.require('goog.testing.jsunit');
</script>
</head>
<body>

<div id="testLinkContainer">
  <a bind="href:url">test link</a>
</div>

<div id="testContainer">
</div>
<script>
// Note: DOMNodeInserted/Removed only fire in webkit if the node is rooted in
// document. This is just an attachment point so that tests will pass in
// webkit.
var testContainerDiv = document.getElementById('testContainer');
</script>

<script>

function testBindAttributeDeclaration() {
  var div = document.getElementById('testLinkContainer');
  var anchor = div.children[0];
  assertEquals('href:url', anchor.bind);

  div.model = {url: 'http://code.google.com/'};
  Model.dirtyCheck();
  assertEquals('http://code.google.com/', anchor.href);

  anchor.model = {url: 'http://github.com/'};
  Model.dirtyCheck();
  assertEquals('http://github.com/', anchor.href);

  document.body.removeChild(div);
}

function testBindAttributeDynamic() {
  var div = document.createElement('div');
  div.model = {url: 'http://a.com/'};
  document.body.appendChild(div);
  div.innerHTML = '<a bind="href:url">link</a>';
  Model.dirtyCheck();
  assertEquals('http://a.com/', div.children[0].href);

  document.body.removeChild(div);
}

function testBindProperty() {
  var el = document.createElement('input');
  el.model = {text: 'hello world'};
  el.bind = 'value: text';
  Model.dirtyCheck();
  assertEquals('hello world', el.value);

  el.model.text = 'goodbye';
  Model.dirtyCheck();
  assertEquals('goodbye', el.value);
}

function testFormat() {
  var b = new Binding('a.b', 'a.c');
  var data = {
    a: {
      b: 'B',
      c: 'C',
    }
  };
  Model.dirtyCheck();
  assertEquals('B, C', b.format(Model.getValueAtPath(data, 'a.b'),
                                Model.getValueAtPath(data, 'a.c')));
}

function testSimpleBinding() {
  var el = document.createElement('div');
  el.model = {a: '1'};
  el.addBinding('foo', new Binding('a'));
  Model.dirtyCheck();
  assertEquals('1', el.foo);

  el.model.a = '2';
  Model.dirtyCheck();
  assertEquals('2', el.foo);

  el.model.a = 232.2;
  Model.dirtyCheck();
  assertEquals(232.2, el.foo);

  el.model.a = 232;
  Model.dirtyCheck();
  assertEquals(232, el.foo);

  el.model.a = null;
  Model.dirtyCheck();
  assertEquals(null, el.foo);

  el.model.a = undefined;
  Model.dirtyCheck();
  assertNull(el.foo);
}

function testSimpleBindingAbsoluteRelativeAndCurrent() {
  var parent = document.createElement('div');
  parent.modelScope = 'abs';
  var child = parent.appendChild(document.createElement('div'))
  child.modelScope = 'ans';
  var el = child.appendChild(document.createElement('div'))
  el.modelScope = 'cur';

  parent.model = {
    val: 'root',
    abs: {
      val: 'abs',
      ans: {
        val: 'ans',
        cur: 'cur'
      }
    }
  };

  el.addBinding('root', '{{ /val }}');
  Model.dirtyCheck();
  assertEquals('root', el.root);

  el.addBinding('abs', '{{ /abs.val }}');
  Model.dirtyCheck();
  assertEquals('abs', el.abs);

  el.addBinding('ans', '{{ ../val }}');
  Model.dirtyCheck();
  assertEquals('ans', el.ans);

  el.addBinding('cur', '{{ ./ }}');
  Model.dirtyCheck();
  assertEquals('cur', el.cur);
}

function testSimpleBindingWithDashes() {
  var el = document.createElement('div');
  el.model = {a: '1'};
  el.addBinding('foo-bar', new Binding('a'));
  Model.dirtyCheck();
  assertEquals('1', el.fooBar);

  el.model.a = '2';
  Model.dirtyCheck();
  assertEquals('2', el.fooBar);
}

function testBindToForeignNode() {
  var el = document.createElement('div');
  el.model = {a: '1'};
  var other = document.createElement('div');
  other.model = {a: '2'};

  el.addBinding('foo', new Binding(other, 'a'));

  Model.dirtyCheck();
  assertEquals('2', el.foo);
  el.model.a = '3';
  other.model.a = '4';
  Model.dirtyCheck();
  assertEquals('4', el.foo);
}

function testBindToNonNodeModel() {
  var el = document.createElement('div');
  var m = {a: '1'};

  el.addBinding('foo', new Binding(m, 'a'));

  Model.dirtyCheck();
  assertEquals('1', el.foo);
  m.a = '2';
  Model.dirtyCheck();
  assertEquals('2', el.foo);
}

function testBindToNamedModel() {
  var el = document.createElement('div');
  document.models = {a: '1'};

  el.addBinding('foo', new Binding('#a'));
  Model.dirtyCheck();
  assertEquals('1', el.foo);

  document.models.a = '2';
  Model.dirtyCheck();
  assertEquals('2', el.foo);
}

function testSimpleBindingChangeModel() {
  var el = document.createElement('div');
  el.addBinding('foo', new Binding('a'));
  el.model = {a: '1'};
  Model.dirtyCheck();
  assertEquals('1', el.foo);
}

function testSimpleBindingChangeModelScope() {
  var el = document.createElement('div');
  el.addBinding('foo', new Binding('a'));
  el.model = {a: {a: '1'}};
  Model.dirtyCheck();
  assertObjectEquals({a: '1'}, el.foo);

  el.modelScope = 'a';
  Model.dirtyCheck();
  assertEquals('1', el.foo);
}

function testBindingChangeAtModelScopeChild() {
  var parent = document.createElement('div');
  var child = parent.appendChild(document.createElement('div'));

  parent.model = {
    a: {b: 1}
  };

  child.addBinding('b', new Binding('b'));
  child.modelScope = 'a';
  Model.dirtyCheck();
  assertEquals(1, child.b);

  parent.model.a = {b: 2};
  Model.dirtyCheck();
  assertEquals(2, child.b);
}

function testBindingChangeAtModelScopeParent() {
  var parent = document.createElement('div');
  var child = parent.appendChild(document.createElement('div'));

  parent.model = {
    a: {b: 1}
  };

  child.addBinding('b', new Binding('b'));
  parent.modelScope = 'a';
  Model.dirtyCheck();
  assertEquals(1, child.b);

  parent.model.a = {b: 2};
  Model.dirtyCheck();
  assertEquals(2, child.b);
}

function testSimpleBindingChangeAncestorModel() {
  var d1 = document.createElement('div');
  var d2 = d1.appendChild(document.createElement('div'));
  var d3 = d2.appendChild(document.createElement('div'));
  d3.addBinding('foo', new Binding('a'));

  d1.model = {a: 1};
  Model.dirtyCheck();
  assertEquals(1, d3.foo);

  d1.model = {a: 2};
  Model.dirtyCheck();
  assertEquals(2, d3.foo);

  d2.model = {a: 3};
  Model.dirtyCheck();
  assertEquals(3, d3.foo);

  d3.model = {a: 4};
  Model.dirtyCheck();
  assertEquals(4, d3.foo);

  d2.model = {a: 5};
  Model.dirtyCheck();
  assertEquals(4, d3.foo);

  d3.model = undefined;
  Model.dirtyCheck();
  assertEquals(5, d3.foo);

  d2.model = undefined;
  Model.dirtyCheck();
  assertEquals(2, d3.foo);
}

function testSimpleBindingChangeAncestorModelScope() {
  var d1 = document.createElement('div');
  var d2 = d1.appendChild(document.createElement('div'));
  var d3 = d2.appendChild(document.createElement('div'));
  d3.addBinding('foo', new Binding('c'));
  d3.addBinding('bar', new Binding('d'));
  d3.addBinding('baz', new Binding('c.d'));

  d1.model = {
    a: {c: {d: 1}},
    b: {c: {d: 2}},
    c: {c: {d: 3}},
  };
  Model.dirtyCheck();
  assertObjectEquals({c: {d: 3}}, d3.foo);
  assertNull(d3.bar);
  assertNull(d3.baz);

  d1.modelScope = 'a';
  Model.dirtyCheck();
  assertObjectEquals({d: 1}, d3.foo);
  assertNull(d3.bar);
  assertEquals(1, d3.baz);

  d1.modelScope = 'b';
  Model.dirtyCheck();
  assertObjectEquals({d: 2}, d3.foo);
  assertNull(d3.bar);
  assertEquals(2, d3.baz);

  d2.modelScope = 'c';
  Model.dirtyCheck();
  assertNull(d3.foo);
  assertEquals(2, d3.bar);
  assertNull(d3.baz);

  d1.modelScope = 'c';
  Model.dirtyCheck();
  assertNull(d3.foo);
  assertEquals(3, d3.bar);
  assertNull(d3.baz);
}

function testBindToModelScope() {
  var m = {
    scope: 'foo',
    foo: {x: 'abc'},
    bar: {x: 'def'}
  };

  var d1 = document.createElement('div');
  d1.model = m;
  d1.modelScope = 'foo';
  d1.addBinding('textContent', '{{x}}');
  Model.dirtyCheck();
  assertEquals('abc', d1.textContent);

  d1.addBinding('modelScope', '{{scope}}');
  m.scope = 'bar';
  Model.dirtyCheck();
  assertEquals('def', d1.textContent);
  assertEquals('bar', d1.modelScope);
}

function testPlaceHolderBindingText() {
  var phb = new PlaceHolderBinding('Hello {{ adj }} {{noun}}!');
  var m = {
    adj: 'cruel',
    noun: 'world'
  };

  var el = document.createElement('div');
  el.textContent = 'dummy';
  el.firstChild.addBinding('textContent', phb);
  el.model = m;

  Model.dirtyCheck();
  assertEquals('Hello cruel world!', el.textContent);

  el.model.adj = 'happy';
  Model.dirtyCheck();
  assertEquals('Hello happy world!', el.textContent);

  el.model = {
    adj: 'sunny',
    noun: 'day'
  };
  Model.dirtyCheck();
  assertEquals('Hello sunny day!', el.textContent);
}

function testPlaceHolderBindingText2() {
  var phb = new PlaceHolderBinding('Hello {{ adj }} {{noun}}!');
  var m = {
    adj: 'cruel',
    noun: 'world'
  };

  var el = document.createElement('div');
  el.textContent = 'dummy';
  el.firstChild.binding = phb;
  el.model = m;

  Model.dirtyCheck();
  assertEquals('Hello cruel world!', el.textContent);

  el.model.adj = 'happy';
  Model.dirtyCheck();
  assertEquals('Hello happy world!', el.textContent);

  el.model = {
    adj: 'sunny',
    noun: 'day'
  };
  Model.dirtyCheck();
  assertEquals('Hello sunny day!', el.textContent);
}

function testPlaceHolderBindingTextInline() {
  var m = {
    adj: 'cruel',
    noun: 'world'
  };

  var el = document.createElement('div');
  el.textContent = 'dummy';
  el.firstChild.addBinding('textContent', 'Hello {{ adj }} {{noun}}!');
  el.model = m;

  Model.dirtyCheck();
  assertEquals('Hello cruel world!', el.textContent);

  el.model.adj = 'happy';
  Model.dirtyCheck();
  assertEquals('Hello happy world!', el.textContent);

  el.model = {
    adj: 'sunny',
    noun: 'day'
  };
  Model.dirtyCheck();
  assertEquals('Hello sunny day!', el.textContent);
}

function testPlaceHolderBindingElementProperty() {
  var phb = new PlaceHolderBinding('Hello {{ adj }} {{noun}}!');
  var m = {
    adj: 'cruel',
    noun: 'world'
  };

  var el = document.createElement('div');
  el.addBinding('foo', phb);
  el.model = m;

  Model.dirtyCheck();
  assertEquals('Hello cruel world!', el.foo);

  el.model.adj = 'happy';
  Model.dirtyCheck();
  assertEquals('Hello happy world!', el.foo);

  el.model = {
    adj: 'sunny',
    noun: 'day'
  };
  Model.dirtyCheck();
  assertEquals('Hello sunny day!', el.foo);

  // Change the binding.
  el.addBinding('foo', 'Goodbye {{ adj }} {{noun}}!');
  Model.dirtyCheck();
  assertEquals('Goodbye sunny day!', el.foo);

  // Remove the binding. Should stop following the model.
  el.removeBinding('foo');
  el.model.adj = 'cloudy';
  Model.dirtyCheck();
  assertEquals('Goodbye sunny day!', el.foo);
}

function testDomTreeChanges() {
  var d1 = document.createElement('div');
  d1.id = 'd1';
  var d2 = document.createElement('div');
  d2.id = 'd2';
  var d3 = document.createElement('div');
  d3.id = 'd3';
  d3.addBinding('foo', new Binding('a'));

  testContainerDiv.appendChild(d1);
  testContainerDiv.appendChild(d2);

  Model.dirtyCheck();
  assertNull(d3.foo);

  d1.model = {a: 1};
  d2.model = {a: 2};

  d1.appendChild(d3);
  Model.dirtyCheck();
  assertEquals(1, d3.foo);

  d2.appendChild(d3);
  Model.dirtyCheck();
  assertEquals(2, d3.foo);

  testContainerDiv.innerHTML = '';
}

function dispatchEvent(type, target) {
  var event = document.createEvent('HTMLEvents');
  event.initEvent(type, true, false);
  target.dispatchEvent(event);
}

function testInputElementTextBinding() {
  var m = {val: 'ping'};

  var el = document.createElement('input');
  el.addBinding('value', '{{val}}');
  el.model = m;
  Model.dirtyCheck();
  assertEquals('ping', el.value);

  el.value = 'pong';
  dispatchEvent('input', el);
  assertEquals('pong', m.val);

  // Try a deep path.
  m = {
    a: {
      b: {
        c: 'ping'
      }
    }
  };

  el.addBinding('value', '{{ a.b.c }}');
  el.model = m;
  Model.dirtyCheck();
  assertEquals('ping', el.value);

  el.value = 'pong';
  dispatchEvent('input', el);
  assertEquals('pong', Model.getValueAtPath(m, 'a.b.c'));

  // Start with the model property being absent.
  delete m.a.b.c;
  Model.dirtyCheck();
  assertEquals('', el.value);

  el.value = 'pong';
  dispatchEvent('input', el);
  assertEquals('pong', Model.getValueAtPath(m, 'a.b.c'));

  // Model property unreachable (and unsettable).
  delete m.a.b;
  Model.dirtyCheck();
  assertEquals('', el.value);

  el.value = 'pong';
  dispatchEvent('input', el);
  assertEquals(undefined, Model.getValueAtPath(m, 'a.b.c'));
}

function testSimpleTransform() {
  var trans = {
    toTarget: function(source) {
      return source + 1;
    },

    toSource: function(target) {
      return (+target) - 1;
    }
  };

  var m = {val: 1};
  var el = document.createElement('input');
  el.addBinding('value', new Binding('val', trans));
  el.model = m;
  Model.dirtyCheck();
  assertEquals('2', el.value);

  el.value = '3';
  dispatchEvent('input', el);
  assertEquals(2, m.val);
}

function testDeclarativeTransform() {
  var transformArgs;
  var toTargetArgs;

  function MyTrans(var_arg) {
    transformArgs = Array.prototype.slice.call(arguments);
  }
  MyTrans.prototype = {
    toTarget: function(source, elm, property) {
      toTargetArgs = Array.prototype.slice.call(arguments);
      return source + 1;
    },

    toSource: function(target) {
      return +target - 1;
    }
  };

  Transform.registry.myTrans = MyTrans;

  var m = {val: 1};
  var el = document.createElement('input');
  el.addBinding('value',
                '{{ val | myTrans("a", \'b\', 1) }}');
  el.model = m;
  Model.dirtyCheck();
  assertEquals('2', el.value);
  assertArrayEquals([m.val, 'val', el, 'value'], toTargetArgs);

  el.value = '3';
  dispatchEvent('input', el);
  assertEquals(2, m.val);

  assertArrayEquals(['a', 'b', 1],
                    transformArgs);

  delete Transform.registry.myTrans;
}

function testClassListTransform() {
  var el = document.createElement('div');
  el.addBinding('className', '{{ val | toggle("selected") }}');
  el.model = { val: false };
  Model.dirtyCheck();
  assertEquals('', el.className);

  el.model.val = true;
  Model.dirtyCheck();
  assertEquals('selected', el.className);

  // Test impicit naming
  el = document.createElement('div');
  el.addBinding('className', '{{ selected | toggle }}');
  el.model = { selected: false };
  Model.dirtyCheck();
  assertEquals('', el.className);

  el.model.selected = true;
  Model.dirtyCheck();
  assertEquals('selected', el.className);

  // Test impicit naming
  el = document.createElement('div');
  el.addBinding('className', 'classA {{ selected | toggle }} classB');
  el.model = { selected: false };
  Model.dirtyCheck();
  assertEquals('classA  classB', el.className);

  el.model.selected = true;
  Model.dirtyCheck();
  assertEquals('classA selected classB', el.className);
}

function testBindAttributeTransform() {
  var transformArgs;
  var toTargetArgs;

  function MyTrans(var_arg) {
    transformArgs = Array.prototype.slice.call(arguments);
  }
  MyTrans.prototype = {
    toTarget: function(source, elm, property) {
      toTargetArgs = Array.prototype.slice.call(arguments);
      return source + 1;
    },

    toSource: function(target) {
      return +target - 1;
    }
  };

  Transform.registry.myTrans = MyTrans;

  var m = {val: 1};
  var el = document.createElement('input');
  el.bind = 'value: val | myTrans("a", \'b\', 1)';
  el.model = m;
  Model.dirtyCheck();
  assertEquals('2', el.value);
  assertArrayEquals([m.val, 'val', el, 'value'], toTargetArgs);

  el.value = '3';
  dispatchEvent('input', el);
  assertEquals(2, m.val);

  assertArrayEquals(['a', 'b', 1],
                    transformArgs);

  delete Transform.registry.myTrans;
}

function testInputElementCheckbox() {
  var m = {val: true};

  var el = document.createElement('input');
  el.type = 'checkbox';
  el.addBinding('checked', new Binding('val'));
  el.model = m;
  Model.dirtyCheck();
  assertEquals(true, el.checked);

  m.val = false;
  Model.dirtyCheck();
  assertEquals(false, el.checked);

  el.checked = true;
  dispatchEvent('click', el);
  assertEquals(true, m.val);

  el.checked = false;
  dispatchEvent('click', el);
  assertEquals(false, m.val);
}

function testInputElementRadio() {
  var m = {val1: true, val2: false, val3: false, val4: true};
  var RADIO_GROUP_NAME = 'test';

  var container = document.body.appendChild(document.createElement('div'));
  container.model = m;

  var el1 = container.appendChild(document.createElement('input'));
  el1.type = 'radio';
  el1.name = RADIO_GROUP_NAME;
  el1.addBinding('checked', new Binding('val1'));

  var el2 = container.appendChild(document.createElement('input'));
  el2.type = 'radio';
  el2.name = RADIO_GROUP_NAME;
  el2.addBinding('checked', new Binding('val2'));

  var el3 = container.appendChild(document.createElement('input'));
  el3.type = 'radio';
  el3.name = RADIO_GROUP_NAME;
  el3.addBinding('checked', new Binding('val3'));

  var el4 = container.appendChild(document.createElement('input'));
  el4.type = 'radio';
  el4.name = 'othergroup';
  el4.addBinding('checked', new Binding('val4'));

  Model.dirtyCheck();
  assertEquals(true, el1.checked);
  assertEquals(false, el2.checked);
  assertEquals(false, el3.checked);
  assertEquals(true, el4.checked);

  m.val1 = false;
  m.val2 = true;
  Model.dirtyCheck();
  assertEquals(false, el1.checked);
  assertEquals(true, el2.checked);
  assertEquals(false, el3.checked);
  assertEquals(true, el4.checked);

  el1.checked = true;
  dispatchEvent('change', el1);
  assertEquals(true, m.val1);
  assertEquals(false, m.val2);
  assertEquals(false, m.val3);
  assertEquals(true, m.val4);

  el3.checked = true;
  dispatchEvent('change', el3);
  assertEquals(false, m.val1);
  assertEquals(false, m.val2);
  assertEquals(true, m.val3);
  assertEquals(true, m.val4);

  document.body.removeChild(container);
}

function testInputElementRadioMultipleForms() {
  var m = {val1: true, val2: false, val3: false, val4: true};
  var RADIO_GROUP_NAME = 'test';

  var container = document.body.appendChild(document.createElement('div'));
  container.model = m;
  var form1 = container.appendChild(document.createElement('form'));
  var form2 = container.appendChild(document.createElement('form'));

  var el1 = form1.appendChild(document.createElement('input'));
  el1.type = 'radio';
  el1.name = RADIO_GROUP_NAME;
  el1.addBinding('checked', new Binding('val1'));

  var el2 = form1.appendChild(document.createElement('input'));
  el2.type = 'radio';
  el2.name = RADIO_GROUP_NAME;
  el2.addBinding('checked', new Binding('val2'));

  var el3 = form2.appendChild(document.createElement('input'));
  el3.type = 'radio';
  el3.name = RADIO_GROUP_NAME;
  el3.addBinding('checked', new Binding('val3'));

  var el4 = form2.appendChild(document.createElement('input'));
  el4.type = 'radio';
  el4.name = RADIO_GROUP_NAME;
  el4.addBinding('checked', new Binding('val4'));

  Model.dirtyCheck();
  assertEquals(true, el1.checked);
  assertEquals(false, el2.checked);
  assertEquals(false, el3.checked);
  assertEquals(true, el4.checked);

  el2.checked = true;
  dispatchEvent('change', el2);
  assertEquals(false, m.val1);
  assertEquals(true, m.val2);

  // Radio buttons in form2 should be unaffected
  assertEquals(false, m.val3);
  assertEquals(true, m.val4);

  el3.checked = true;
  dispatchEvent('change', el3);
  assertEquals(true, m.val3);
  assertEquals(false, m.val4);

  // Radio buttons in form1 should be unaffected
  assertEquals(false, m.val1);
  assertEquals(true, m.val2);

  document.body.removeChild(container);
}

function testSelectElementValue() {
  var m = {value: 'foo'};

  // Value
  var el = document.createElement('select');
  var op = el.appendChild(document.createElement('option'));
  op.textContent = 'Foo';
  op.value = 'foo';
  var op = el.appendChild(document.createElement('option'));
  op.textContent = 'Bar';
  op.value = 'bar';

  el.addBinding('value', new Binding('value'));
  el.model = m;

  Model.dirtyCheck();
  assertEquals('foo', el.value);
  assertEquals('foo', m.value);

  m.value = 'bar';
  Model.dirtyCheck();
  assertEquals('bar', el.value);

  el.value = 'foo';
  dispatchEvent('change', el);
  assertEquals('foo', m.value);
}

function testSelectElementSelectedIndex() {
  var m = {index: 0};

  // Value
  var el = document.createElement('select');
  var op = el.appendChild(document.createElement('option'));
  op.textContent = 'Foo';
  op.value = 'foo';
  var op = el.appendChild(document.createElement('option'));
  op.textContent = 'Bar';
  op.value = 'bar';

  el.addBinding('selectedIndex', new Binding('index'));
  el.model = m;

  Model.dirtyCheck();
  assertEquals(0, el.selectedIndex);
  assertEquals(0, m.index);

  m.index = 1;
  Model.dirtyCheck();
  assertEquals(1, el.selectedIndex);

  el.selectedIndex = 0;
  dispatchEvent('change', el);
  assertEquals(0, m.index);
}

function testInputElementTextBindingWithModelScope() {
  var div = document.createElement('div');
  var child = div.appendChild(document.createElement('div'));
  var input = child.appendChild(document.createElement('input'));

  var m = {
    a: {}
  };
  div.model = m;

  child.modelScope = 'a';
  input.addBinding('value', '{{b}}');

  input.value = 'changed';
  dispatchEvent('input', input);
  assertEquals('changed', m.a.b);
}

function testBindToChecked() {
  var div = document.createElement('div');
  var child = div.appendChild(document.createElement('div'));
  var input = child.appendChild(document.createElement('input'));
  input.type = 'checkbox';

  var m = {
    a: {
      b: false
    }
  };
  div.model = m;

  child.modelScope = 'a';
  input.addBinding('checked', '{{b}}');

  input.checked = true;
  dispatchEvent('click', input);
  assertTrue(m.a.b);

  input.checked = false;
  assertTrue(m.a.b);
  dispatchEvent('click', input);
  assertFalse(m.a.b);
}

function testBindToInlineEventListener() {
  var div = document.createElement('div');

  var m = {
    func: function() {}
  };
  div.model = m;
  div.addBinding('onclick', new Binding('func'));
  assertEquals(m.func, div.onclick);
}

function testExpressionBinding() {
  var el = document.createElement('div');
  var m = el.model = {a: 1, b: 2};
  el.addBinding('foo', '{{a}} + {{b}} = {{ expr(a, b) a + b }}');
  Model.dirtyCheck();
  assertEquals('1 + 2 = 3', el.foo);

  m.a = 4;
  Model.dirtyCheck();
  assertEquals('4 + 2 = 6', el.foo);

  m.b = 8;
  Model.dirtyCheck();
  assertEquals('4 + 8 = 12', el.foo);

  el.model = null;
  Model.dirtyCheck();
  assertEquals(' +  = NaN', el.foo);
}

function testExpressionAttributeBinding() {
  var el = document.createElement('input');
  var m = el.model = {a: 1, b: 2};
  el.bind = 'value: expr(a, b) a + b';
  assertEquals('3', el.value);

  m.a = 4;
  Model.dirtyCheck();
  assertEquals('6', el.value);

  m.b = 8;
  Model.dirtyCheck();
  assertEquals('12', el.value);

  el.model = null;
  Model.dirtyCheck();
  assertEquals('NaN', el.value);
}

function testMultipleReferences() {
  var el = document.createElement('div');
  var m = el.model = {foo: 'bar'};
  el.addBinding('foo', '{{foo}} {{ expr(foo) foo }}');
  assertEquals('bar bar', el.foo);
  el.modelScope = 'foo';
}

function testExpressionBindingThis() {
  var el = document.createElement('div');
  el.id = 'test-element';
  el.addBinding('foo', '{{ expr() this.id }}');
  assertEquals('test-element', el.foo);
}

function testExpressionBindingNoCoerce() {
  var el = document.createElement('div');
  var m = el.model = {
    a: {
      b: 1
    },
    c: {
      d: 2
    }
  };
  el.addBinding('foo', '{{ expr(a.b, c.d) b + d }}');
  assertEquals(3, el.foo);
}

function testExceptionInBinding() {
  var b = new Binding('a');

  b.parse = function() {
    throw 'Parse exception';
  };
  b.format = function() {
    throw 'Format exception';
  }

  var el = document.createElement('input');
  el.model = {a: '1'};
  el.addBinding('value', b);
  assertEquals('', el.value);

  el.model.a = '2';
  Model.dirtyCheck();
  assertEquals('', el.value);

  dispatchEvent('input', el);
  assertEquals(undefined, el.model.a);
}

function testExceptionInTransform() {
  var t = new IdentityTransform();
  t.toTarget = function() {
    throw 'toTarget exception';
  }
  t.toSource = function() {
    throw 'toSource exception';
  }

  var b = new Binding('a', t);

  var el = document.createElement('input');
  el.model = {a: '1'};
  el.addBinding('value', b);
  assertEquals('', el.value);

  el.model.a = '2';
  Model.dirtyCheck();
  assertEquals('', el.value);

  dispatchEvent('input', el);
  assertEquals(undefined, el.model.a);
}

function testMinimalReads() {
  var accessesPerNotifyObservers = Model.observableObjects_ ? 1 : 2;
  var el = document.createElement('div');
  function Data(a, b) {
    this.a_ = a;
    this.b_ = b;
  }
  var aAccess = 0;
  var bAccess = 0;
  Data.prototype = {
    get a() {
      aAccess++;
      return this.a_;
    },
    set a(a) {
      this.a_ = a;
    },
    get b() {
      bAccess++;
      return this.b_;
    },
    set b(b) {
      this.b_ = b;
    }
  }

  el.model = new Data('a', 'b');
  el.addBinding('foo', '{{a}} {{b}}');

  assertEquals('a b', el.foo);
  assertEquals(1, aAccess);
  assertEquals(1, bAccess);

  el.model.a = 'c';
  Model.dirtyCheck();
  assertEquals('c b', el.foo);
  assertEquals(1 + 1 * accessesPerNotifyObservers, aAccess);
  assertEquals(1 + 1 * accessesPerNotifyObservers, bAccess);

  el.model.b = 'd';
  Model.dirtyCheck();
  assertEquals('c d', el.foo);
  assertEquals(1 + 2 * accessesPerNotifyObservers, aAccess);
  assertEquals(1 + 2 * accessesPerNotifyObservers, bAccess);
}

function testAsyncBinding() {
  var el = document.createElement('div');
  el.model = {a: '1'};
  var b = new Binding('a');
  el.addBinding('foo', b);

  assertEquals('1', el.foo);

  el.model.a = '2';
  Model.dirtyCheck();
  assertEquals('2', el.foo);

  // Dependency change notifications will just dirty the binding, but not assign
  // to the target;
  b.sync_ = false;
  el.model.a = '3';
  Model.dirtyCheck();
  assertEquals('2', el.foo);

  // Since the binding is now dirty, setting sync_ = true will trigger
  // assignment.
  b.sync_ = true;
  Model.dirtyCheck();
  assertEquals('3', el.foo);
}

function testBindingToPhantomNode() {
  var root = document.createElement('div');
  var liveChild = root.appendChild(document.createElement('div'));
  liveChild.modelScope = 'foo';
  liveChild.addBinding('a', new Binding('bar'));

  var b = new Binding('bat');
  b.sync_ = false;

  var valueBinding = new Binding('text');
  valueBinding.sync_ = false;

  var phantomChild = {
    modelScope: 'baz',
    parentElement: root,
    bindings_: {
      'b': b,
      'value': valueBinding
    }
  };

  b.bindTo(phantomChild, 'b');
  valueBinding.bindTo(phantomChild, 'value');

  assertEquals(root, phantomChild.modelOwner_);

  root.model = {
    foo: {bar: 1},
    baz: {bat: 2, text: 'test'}
  };
  Model.dirtyCheck();
  assertEquals(1, liveChild.a);

  root.model = {
    foo: {bar: 3},
    baz: {bat: 4, text: 'test2'}
  };
  Model.dirtyCheck();
  assertEquals(3, liveChild.a);

  root.model.baz = {bat: 5, text: 'test3'};

  // Move the modelOwner
  var newRoot = document.createElement('div');
  newRoot.appendChild(root);
  newRoot.model = root.model;
  root.model = undefined;

  // Make sure the phantomChild knows it's got a new modelOwner
  Model.dirtyCheck();
  assertEquals(newRoot, phantomChild.modelOwner_);

  // Simulate turning phantomChild into a real child and re-assign.
  var newChild = root.appendChild(document.createElement('input'));
  newChild.modelScope = phantomChild.modelScope;
  newChild.bindings_ = newChild.bindings_ || {};

  newChild.bindings_['b'] = b;
  b.rebindTo(newChild);
  b.sync_ = true;
  newChild.bindings_['value'] = valueBinding;
  valueBinding.rebindTo(newChild);
  valueBinding.sync_ = true;

  Model.dirtyCheck();
  assertEquals(5, newChild.b);
  assertEquals('test3', newChild.value);

  // Verify that the two-way binding got setup correctly.
  newChild.value = 'test4';
  dispatchEvent('input', newChild);
  assertEquals('test4', newRoot.model.baz.text);
}

</script>
</body>
</html>
